#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
THN DATA ABSTRACTION LAYER version 1

Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

This library is dual-licensed.
  + For commercial software, please obtain a commercial license from THN Solutions LLC.

  + For free software, this library is licensed under GPL version 3. A summary of GPLv3 is
    listed below. You should also find a copy of GPLv3 as file License_GPLv3.txt included
	with the source-code files.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 3 as published by
    the Free Software Foundation  of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

#region Using directives
using System;
using System.Collections;
using Thn.Application;
using Thn.Data.Query;
#endregion

namespace Thn.Data.Persistence
{
    /// <summary>
    /// A persistent context is a single-thread, single-user persistence service.
    /// <para>This context provides object caching, command logging, as well as relationship handling.</para>
    /// </summary>
    public class PersistentContext : IPersistentContext, IQueryProcessor, IObjectStore, ICommandCollectionParent
    {
        #region Cache
        private IPersistentCache mCache;
        /// <summary>
        /// Gets/Sets the cache this context uses for storing objects
        /// </summary>
        public IPersistentCache Cache
        {
            get { return mCache; }
            set { mCache = value; }
        }
        #endregion
	
        #region Add
        #region Add one object
        /// <summary>
        /// Insert new object
        /// </summary>
        public virtual void Add(IPersistentObject value)
        {
            //attach the object to this context
            value.Context = this;            
            
            //insert an ADD OBJECT command
            LogAdd(value);
            
            //cache for later use
            if (mCache != null) mCache.Save(value);
        }
        #endregion

        #region Add array of objects
        /// <summary>
        /// Insert an array of objects
        /// </summary>
        public virtual void Add(params IPersistentObject[] values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Length; i++)
                {
                    Add(values[i]);
                }
            }
        }
        #endregion

        #region Add list of objects
        /// <summary>
        /// Insert a list of objects
        /// </summary>
        public virtual void Add(IList values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Count; i++)
                {
                    Add((IPersistentObject)values[i]);
                }
            }
        }
        #endregion
        #endregion

        #region Update
        #region Update one object
        /// <summary>
        /// Delete an object
        /// </summary>
        public virtual void Update(IPersistentObject value)
        {
            //attach the object to this context
            value.Context = this;

            //insert a Update OBJECT command
            LogUpdate(value);
        }
        #endregion

        #region Update array of objects
        /// <summary>
        /// Delete an array of objects
        /// </summary>
        public virtual void Update(params IPersistentObject[] values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Length; i++)
                {
                    Update(values[i]);
                }
            }
        }
        #endregion

        #region Update list of objects
        /// <summary>
        /// Delete a list of objects
        /// </summary>
        public virtual void Update(IList values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Count; i++)
                {
                    Update((IPersistentObject)values[i]);
                }
            }
        }
        #endregion
        #endregion

        #region Delete
        #region Delete one object
        /// <summary>
        /// Delete an object
        /// </summary>
        public virtual void Delete(IPersistentObject value)
        {
            //attach the object to this context
            value.Context = this;

            //insert a REMOVE OBJECT command
            LogRemove(value);

            //remove from cache
            if (mCache != null) mCache.Remove(value);
        }
        #endregion

        #region Delete array of objects
        /// <summary>
        /// Delete an array of objects
        /// </summary>
        public virtual void Delete(params IPersistentObject[] values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Length; i++)
                {
                    Delete(values[i]);
                }
            }
        }
        #endregion

        #region Delete list of objects
        /// <summary>
        /// Delete a list of objects
        /// </summary>
        public virtual void Delete(IList values)
        {
            if (values != null)
            {
                for (int i = 0; i < values.Count; i++)
                {
                    Delete((IPersistentObject)values[i]);
                }
            }
        }
        #endregion
        #endregion

        #region Do Property Changed
        /// <summary>
        /// Signal this context that a persistent object's property has been changed
        /// </summary>
        internal void DoPropertyChanged(IPersistentObject value, string propertyName, object oldValue, object newValue)
        {
            LogUpdate(value, propertyName, oldValue, newValue);

            //raise event
            RaiseChanged();
        }
        #endregion

        #region Select
        /// <summary>
        /// Gets a new select query
        /// </summary>
        public SelectQuery Select
        {
            get
            {
                SelectQuery qry = new SelectQuery();
                qry.Processor = this;
                return qry;
            }
        }
        #endregion

        #region Parent
        private IPersistentLayer mParent;
        /// <summary>
        /// Gets the persistent layer that this context belongs to
        /// </summary>
        public IPersistentLayer Parent
        {
            get { return mParent; }
        }
        #endregion	

        #region Commands
        private CommandCollection mCommands;
        /// <summary>
        /// Gets the main collection of commands
        /// </summary>
        public CommandCollection Commands
        {
            get { return mCommands; }
            set
            {
                if (mCommands != value)
                {
                    //remove me as the parent for previous collection
                    if (mCommands != null) mCommands.Parent = null;

                    mCommands = value;

                    //assign me as the parent for the new collection
                    if (mCommands != null) mCommands.Parent = this;
                }
            }
        }
        #endregion

        #region Current Commands
        private CommandCollection mCurrentCommands;
        /// <summary>
        /// Gets current collection of commands (this is the commands of the current transaction scope)
        /// </summary>
        protected CommandCollection CurrentCommands
        {
            get { return mCurrentCommands; }
        }
        #endregion
	
        #region Transaction
        /// <summary>
        /// Cache of transaction scopes
        /// <para>key   : scope ID</para>
        /// <para>value : TransactionCommand</para>
        /// </summary>
        Hashtable mScopes = new Hashtable();

        /// <summary>
        /// Start a new transaction scope.
        /// <para>The new scope will be nested within the current scope (if available)</para>
        /// </summary>
        /// <returns>ID of the new transaction scope</returns>
        public object Begin()
        {
            Guid id = Guid.NewGuid();

            TransactionCommand tx = new TransactionCommand(id);
            tx.Parent = CurrentCommands;
            CurrentCommands.Add(tx);
            mCurrentCommands = tx.Commands;//assign the new scope as the current scope
            mCurrentCommands.Parent = this;
            mScopes.Add(id, tx);

            return id;
        }

        /// <summary>
        /// Accept and close a transaction scope
        /// </summary>
        /// <param name="scopeID">ID of the transaction scope</param>
        public void End(object scopeID)
        {
            if (mScopes.Contains(scopeID))
            {
                TransactionCommand tx = (TransactionCommand)mScopes[scopeID];

                //set the current scope back to the transaction's parent
                mCurrentCommands = tx.Parent;
                mCurrentCommands.Parent = this;
            }
        }

        /// <summary>
        /// Discard modifications made within a transaction scope
        /// </summary>
        /// <param name="scopeID">ID of the transaction scope</param>
        public void Cancel(object scopeID)
        {
            if (mScopes.Contains(scopeID))
            {
                TransactionCommand tx = (TransactionCommand)mScopes[scopeID];

                //set the current scope back to the transaction's parent
                mCurrentCommands = tx.Parent;
                mCurrentCommands.Parent = this;
            }
        }
        #endregion

        #region Undo / Redo
        //#region Undo
        ///// <summary>
        ///// Discard the previous command
        ///// </summary>
        //public void Undo()
        //{
        //    throw new NotImplementedException();
        //}
        //#endregion

        //#region Redo
        ///// <summary>
        ///// Re-apply the previously undone command
        ///// </summary>
        //public void Redo()
        //{
        //    throw new NotImplementedException();
        //}
        //#endregion
        #endregion

        #region Persist
        /// <summary>
        /// Save changes to physical database
        /// </summary>
        public void Persist()
        {
            mParent.ProcessCommands(mCommands);
            mCommands.Clear();
        }
        #endregion

        #region Command Logging
        #region Log Add
        /// <summary>
        /// Create a new AddObject command and log it
        /// </summary>
        protected void LogAdd(IPersistentObject value)
        {
            AddObjectCommand cmd = new AddObjectCommand(value);
            LogCommand(cmd);
        }
        #endregion

        #region Log Remove
        /// <summary>
        /// Create a new RemoveObject command and log it
        /// </summary>
        protected void LogRemove(IPersistentObject value)
        {
            RemoveObjectCommand cmd = new RemoveObjectCommand(value);
            LogCommand(cmd);
        }
        #endregion

        #region Log Update
        /// <summary>
        /// Create a new UpdateObject command and log it
        /// </summary>
        /// <param name="value">the object to log</param>
        protected void LogUpdate(IPersistentObject value)
        {
            UpdateObjectCommand cmd = new UpdateObjectCommand(value);
            LogCommand(cmd);
        }

        /// <summary>
        /// Create a new UpdateObject command and log it
        /// </summary>
        /// <param name="value">the object to log</param>
        /// <param name="propertyName">name of the property that has been changed</param>
        /// <param name="oldValue">previous value of the property</param>
        /// <param name="newValue">new value of the property</param>
        protected void LogUpdate(IPersistentObject value, string propertyName, object oldValue, object newValue)
        {
            UpdatePropertyCommand cmd = new UpdatePropertyCommand(value, propertyName, oldValue, newValue);
            LogCommand(cmd);
        }
        #endregion

        #region Log Begin
        ///// <summary>
        ///// Create a new BeginTransaction command and log it
        ///// </summary>
        ///// <param name="scopeID">ID of the transaction scope</param>
        //protected void LogBegin(object scopeID)
        //{
        //    //BeginTransactionCommand cmd = new BeginTransactionCommand(scopeID);
        //    //LogCommand(cmd);
        //}
        #endregion

        #region Log End
        ///// <summary>
        ///// Create a new EndTransaction command and log it
        ///// </summary>
        ///// <param name="scopeID">ID of the transaction scope</param>
        //protected void LogEnd(object scopeID)
        //{
        //    //EndTransactionCommand cmd = new EndTransactionCommand(scopeID);
        //    //LogCommand(cmd);
        //}
        #endregion

        #region Log Command
        /// <summary>
        /// Append a persistent command to log
        /// </summary>
        protected void LogCommand(PersistentCommand command)
        {
            CurrentCommands.Add(command);
        }
        #endregion
        #endregion

        #region Find
        /// <summary>
        /// Find a certain object based on its identifier
        /// </summary>
        /// <param name="objectType">type of the persistent object</param>
        /// <param name="id">identifier of the object to find</param>
        /// <returns>null if no matching object found</returns>
        public IPersistentObject Find(Type objectType, object id)
        {
            IPersistentObject result = null;

            //lookup from cache first
            if (mCache != null) result = mCache.Find(objectType, id);

            //still not found, find in database
            if (result == null)
            {                
                result = (IPersistentObject)Select.From(objectType)
                                            .Where.Equal("ID", (IComparable)id)
                                            .Execute().AsSingleObject;

                if (result != null)
                {//cache for later use
                    if (mCache != null) mCache.Save(result);

                    //toggle status flags
                    if (result is PersistentObject)
                    {
                        PersistentObject po = (PersistentObject)result;
                        po.SetModifiedFlag(false);
                        po.SetPersistentFlag(true);
                    }
                }
            }

            return result;
        }
        #endregion

        #region Modified
        /// <summary>
        /// Gets whether there are modifications need to be persisted
        /// </summary>
        public bool Modified
        { get { return mCommands.Count != 0; } }
        #endregion

        #region Events
        /// <summary>
        /// Raised when there is new activity occured
        /// </summary>
        public event EventHandler Changed;

        /// <summary>
        /// Raise event Changed
        /// </summary>
        void RaiseChanged()
        {
            EventHandler handler = Changed;
            if (handler != null) handler(this, EventArgs.Empty);
        }
        #endregion

        #region IObjectStore Members
        #region Add
        /// <summary>
        /// Insert a new object to store
        /// </summary>
        void IObjectStore.Add(object value)
        {
            if (value is IPersistentObject) this.Add((IPersistentObject)value);
            else Thn.Exceptions.IncompatibleTypeException.Publish(value, typeof(IPersistentObject));
        }
        #endregion

        #region Update
        /// <summary>
        /// Update an existing object
        /// </summary>
        void IObjectStore.Update(object value)
        {
            if (value is IPersistentObject) this.Update((IPersistentObject)value);
            else Thn.Exceptions.IncompatibleTypeException.Publish(value, typeof(IPersistentObject));
        }
        #endregion

        #region Delete
        /// <summary>
        /// Remove an existing object
        /// </summary>
        void IObjectStore.Delete(object value)
        {
            if (value is IPersistentObject) this.Delete((IPersistentObject)value);
            else Thn.Exceptions.IncompatibleTypeException.Publish(value, typeof(IPersistentObject));
        }
        #endregion

        #region Find
        /// <summary>
        /// Lookup a particular object based on its identifier
        /// </summary>
        /// <param name="objectType">type of object to look for</param>
        /// <param name="id">identifier of the object</param>
        /// <returns>null if not found</returns>
        object IObjectStore.Find(Type objectType, object id)
        {
            return this.Find(objectType, id);
        }

        #endregion
        #endregion

        #region IQueryProcessor Members
        #region Execute
        /// <summary>
        /// Execute query
        /// </summary>
        ExecuteResult IQueryProcessor.Execute(QueryBase command)
        {
            ExecuteResult result = mParent.Execute(command);
            
            ProcessQueryResults(result);

            return result;
        }
        #endregion
        #endregion

        #region Process query results
        /// <summary>
        /// Process results returned from a select query
        /// </summary>
        void ProcessQueryResults(ExecuteResult results)
        {
            IList objects = results.AsList;

            for (int i = 0; i < objects.Count; i++)
            {
                object obj = objects[i];
                if (obj is IPersistentObject)
                {
                    IPersistentObject ipo = (IPersistentObject)obj;

                    //put to cache
                    if (mCache != null) mCache.Save(ipo);

                    //attach to this context
                    ipo.Context = this;

                    if (obj is PersistentObject)
                    {
                        //toggle status flags
                        PersistentObject po = (PersistentObject)obj;
                        po.SetModifiedFlag(false);
                        po.SetPersistentFlag(true);
                    }
                }
            }
        }
        #endregion

        #region ICommandCollectionParent Members
        /// <summary>
        /// The collection have been emptied
        /// </summary>
        void ICommandCollectionParent.ItemsCleared(CommandCollection sender)
        {
            RaiseChanged();
        }

        /// <summary>
        /// A value has been inserted to the collection
        /// </summary>
        void ICommandCollectionParent.ItemsAdded(CommandCollection sender, object value)
        {
            RaiseChanged();
        }

        /// <summary>
        /// A value has been removed from the collection
        /// </summary>
        void ICommandCollectionParent.ItemsRemoved(CommandCollection sender, object value)
        {
            RaiseChanged();
        }
        #endregion

        #region Debug - Log Commands
        /// <summary>
        /// Display the commands available in context
        /// </summary>
        void ViewCommands()
        {
            CommandCollection commands = mCommands;
            if (commands.Count > 0)
            {
                object scope = Log.Start("Persistent Commands");
                LogCommands(commands);
                Log.End(scope);
            }
            else Log.Information("Persistent Commands: -empty-");
        }

        /// <summary>
        /// Write commands to log
        /// </summary>
        void LogCommands(CommandCollection commands)
        {
            for (int i = 0; i < commands.Count; i++)
            {
                PersistentCommand cmd = commands[i];
                if (cmd is TransactionCommand)
                {
                    object scope = Log.Start("Transaction");
                    LogCommands(((TransactionCommand)cmd).Commands);
                    Log.End(scope);
                }
                else Log.Information(string.Format("{0} - {1}", cmd.Status, cmd));
            }
        }

        /// <summary>
        /// For debugging purposes, write to log the collection of persistent commands
        /// </summary>
        public void Debug_LogCommands()
        {
            ViewCommands();
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Create a new instance
        /// </summary>
        /// <param name="parent">The parent who owns this context</param>
        public PersistentContext(IPersistentLayer parent)
        {
            mParent = parent;
            mCommands = new CommandCollection();
            mCommands.Parent = this;
            mCurrentCommands = mCommands;
            mCache = new PersistentCache();
        }
        #endregion
    }
}
